﻿/*******************************************************************************
Copyright (c) 2011-2013, PKStudio

http://pkstudio.org

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
of the Software, and to permit persons to whom the Software is furnished to do
so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*******************************************************************************/

using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using System.Text;
using System.Windows.Forms;

namespace FontCreater
{
	/// <summary>
	/// MainForm of FontCreater.
	/// </summary>
	public partial class MainForm : Form, IDisposable
	{
		// Header of binary font.
		private const uint PkbfFileHeader = 0x46424b50;

		// Size of data header.
		private const int PkbfFileHeaderSize = 8;
		private const int CharDataHeaderSize = 1;

		// Default solid brushes.
		private SolidBrush whiteBrush = new SolidBrush(Color.White);
		private SolidBrush blackBrush = new SolidBrush(Color.Black);
		private SolidBrush redBrush = new SolidBrush(Color.Red);
		private SolidBrush blueBrush = new SolidBrush(Color.Blue);

		// The buffer bitmap.
		private int bufferWidth;
		private int bufferHeight;
		private Bitmap bufferBitmap;

		// Flag.
		private bool isDisposed;

		public MainForm()
		{
			InitializeComponent();

			// Add default ASCII characters.
			StringBuilder defaultCharSet = new StringBuilder(96);
			for (int i = 32; i < 127; i++)
			{
				defaultCharSet.Append((char)i);
			}
			textBoxChar.Text = defaultCharSet.ToString();

			// Load all fonts from system.
			foreach (FontFamily font in FontFamily.Families)
			{
				comboBoxFont.Items.Add(font.Name);
			}

			// Select default item for comboboxes.
			comboBoxFont.Text = "Arial";
			comboBoxStyle.SelectedIndex = 0;
			comboBoxEffect.SelectedIndex = 0;
			comboBoxType.SelectedIndex = 0;

			// Use buffer bitmap.
			bufferWidth = pictureBoxFont.Width - 2;
			bufferHeight = pictureBoxFont.Height - 2;
			bufferBitmap = new Bitmap(bufferWidth, bufferHeight);
			pictureBoxFont.Image = bufferBitmap;

			// Set flag.
			isDisposed = false;
		}

		protected override void Dispose(bool disposing)
		{
			if (!isDisposed)
			{
				whiteBrush.Dispose();
				blueBrush.Dispose();
				redBrush.Dispose();
				blackBrush.Dispose();

				bufferBitmap.Dispose();

				isDisposed = true;
			}
			if (disposing)
			{
				if (components != null)
				{
					components.Dispose();
				}
			}
			base.Dispose(disposing);
		}

		private void ButtonStartClick(object sender, EventArgs e)
		{
#if !DEBUG
			try
			{
#endif
				// Get the character set.
				string charSet = textBoxChar.Text;
				if (string.IsNullOrEmpty(charSet))
				{
					MessageBox.Show("Please input some characters at first!");
					return;
				}

				// Get font name.
				string fontName = comboBoxFont.Text;
				if (string.IsNullOrEmpty(fontName))
				{
					MessageBox.Show("Please input font name at first!");
					return;
				}

				// Get font size.
				string fontSizeStr = textBoxSize.Text;
				if (string.IsNullOrEmpty(fontSizeStr))
				{
					MessageBox.Show("Please input font size at first!");
					return;
				}
				float fontSize;
				if (!float.TryParse(fontSizeStr, out fontSize) || fontSize <= 0)
				{
					MessageBox.Show("Font size is invalid!");
					return;
				}

				// Get font style.
				FontStyle fontStyle;
				switch (comboBoxStyle.SelectedIndex)
				{
					default:
						fontStyle = FontStyle.Regular;
						break;
					case 1:
						fontStyle = FontStyle.Bold;
						break;
					case 2:
						fontStyle = FontStyle.Italic;
						break;
					case 3:
						fontStyle = FontStyle.Bold | FontStyle.Italic;
						break;
				}

				Font font = new Font(fontName, fontSize, fontStyle, GraphicsUnit.Pixel);
				int effect = comboBoxEffect.SelectedIndex;
				int exportType = comboBoxType.SelectedIndex;
				switch (exportType)
				{
					case 0:
						this.CreateFont(charSet, font, effect, 1);
						break;
					case 1:
						this.CreateFont(charSet, font, effect, 2);
						break;
					case 2:
						this.CreateFont(charSet, font, effect, 1, true);
						break;
					case 3:
						this.CreateFont(charSet, font, effect, 2, true);
						break;
					case 4:
						this.CreateFont(charSet, font, effect, 4, true);
						break;
					case 5:
						this.CreateFont(charSet, font, effect, 8, true);
						break;
					case 6:
						this.CreateFont(charSet, font, effect, 4, true, true);
						break;
					case 7:
						this.CreateFont(charSet, font, effect, 8, true, true);
						break;
				}
				MessageBox.Show("Font has been genarated!", "Successful");
#if !DEBUG
			}
			catch (Exception exception)
			{
				MessageBox.Show(exception.Message, "Error");
			}
#endif
		}

		private void CreateFont(string charSet, Font font, int effect, int scale)
		{
			this.CreateFont(charSet, font, effect, scale, false, false);
		}

		private void CreateFont(string charSet, Font font, int effect, int scale, bool isBmpFont)
		{
			this.CreateFont(charSet, font, effect, scale, isBmpFont, false);
		}

		private void CreateFont(string charSet, Font font, int effect, int scale, bool isBmpFont, bool needAddGap)
		{
			using (Graphics graphics = Graphics.FromImage(bufferBitmap))
			{
				// Do not use smooth for font.
				graphics.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;

				// Get trim size of font.
				List<Rectangle> charRectList = new List<Rectangle>();
				int trimTop = bufferHeight, trimLeft = bufferWidth, trimBottom = 0, trimRight = 0;
				foreach (char ch in charSet)
				{
					string str = ch.ToString();

					graphics.Clear(blueBrush.Color);
					this.DrawChar(graphics, str, font, effect);
					pictureBoxFont.Refresh();

					SizeF size = graphics.MeasureString(str, font);
					int width = (int)Math.Ceiling(size.Width) + effect;
					int height = (int)Math.Ceiling(size.Height) + effect;
					Rectangle rect = this.GetTrimRect(width, height);

					charRectList.Add(rect);

					if (!rect.IsEmpty)
					{
						if (rect.Top < trimTop)
						{
							trimTop = rect.Top;
						}
						if (rect.Left < trimLeft)
						{
							trimLeft = rect.Left;
						}
						if (rect.Bottom > trimBottom)
						{
							trimBottom = rect.Bottom;
						}
						if (rect.Right > trimRight)
						{
							trimRight = rect.Right;
						}
					}
				}
				Rectangle trimRect = new Rectangle(trimLeft, trimTop, trimRight - trimLeft, trimBottom - trimTop);
				MessageBox.Show(string.Format("Font trim size - width:{0}, height:{1}", trimRect.Width, trimRect.Height));

				if (!isBmpFont)
				{
					// Generate binary font.
					using (BinaryWriter writer = new BinaryWriter(File.Open(font.Name + ".pkbf", FileMode.Create)))
					{
						int wide = GetMemoryAlignment(trimRect.Width * scale);
						byte[] data = new byte[wide * trimRect.Height * scale + CharDataHeaderSize];
						int length = charSet.Length;
						int index = 0;

						writer.Write(PkbfFileHeader);
						writer.Write((ushort)length);
						writer.Write((byte)wide);
						writer.Write((byte)trimRect.Height * scale);

						foreach (char ch in charSet)
						{
							string str = ch.ToString();

							graphics.FillRectangle(blueBrush, trimRect);
							this.DrawChar(graphics, str, font, effect);
							pictureBoxFont.Refresh();

							if (ch == ' ')
							{
								this.GenarateData(data, new Rectangle(trimRect.X, trimRect.Y, trimRect.Height / 2, trimRect.Height), wide, scale);
							}
							else if (ch == '　')
							{
								this.GenarateData(data, new Rectangle(trimRect.X, trimRect.Y, trimRect.Height, trimRect.Height), wide, scale);
							}
							else
							{
								Rectangle rect = charRectList[index];
								this.GenarateData(data, new Rectangle(rect.X, trimRect.Y, rect.Width, trimRect.Height), wide, scale);
							}

							writer.Seek(index * 2 + PkbfFileHeaderSize, SeekOrigin.Begin);
							writer.Write((ushort)ch);
							writer.Seek(length * 2 + PkbfFileHeaderSize + index * data.Length, SeekOrigin.Begin);
							writer.Write(data);

							index++;
						}
					}
				}
				else
				{
					// Generate bitmap font.
					using (StreamWriter writer = new StreamWriter(font.Name + ".fnt"))
					{
						int totalWidth = trimRect.Width * charSet.Length * scale;
						int totalHeight = trimRect.Height * scale;
						int charEachLine = (int)Math.Sqrt(totalWidth / totalHeight);
						totalWidth /= charEachLine;
						totalHeight *= charEachLine;
						for (int size = 256; true; size *= 2)
						{
							if (size >= totalWidth && size >= totalHeight)
							{
								totalWidth = totalHeight = size;
								break;
							}
							if (size == 2048)
							{
								// iOS support maximum size is 2048x2048.
								throw new ArgumentOutOfRangeException("font", "Too many characters or too large font size.");
							}
						}

						writer.WriteLine("info face=\"{0}\" size={1} bold=0 italic=0 charset=\"\" unicode=0 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1", font.Name, font.Size);
						writer.WriteLine("common lineHeight={0} base=26 scaleW={1} scaleH={2} pages=1 packed=0", trimRect.Height + scale * 2, totalWidth, totalHeight);
						writer.WriteLine("page id=0 file=\"{0}.png\"", font.Name);
						writer.WriteLine("chars count={0}", charSet.Length);

						int nextX = 0, nextY = 0;
						int index = 0;
						Bitmap bitmap = new Bitmap(totalWidth, totalHeight, PixelFormat.Format32bppArgb);
						foreach (char ch in charSet)
						{
							string str = ch.ToString();

							graphics.FillRectangle(blueBrush, trimRect);
							this.DrawChar(graphics, str, font, effect);
							pictureBoxFont.Refresh();

							if (ch == ' ')
							{
								writer.WriteLine("char id=32 x=0 y=0 width=0 height=0 xoffset=0 yoffset={0} xadvance={1} page=0 chnl=0", scale, trimRect.Width * scale / 2 + scale);
							}
							else if (ch == '　')
							{
								writer.WriteLine("char id={2} x=0 y=0 width=0 height=0 xoffset=0 yoffset={0} xadvance={1} page=0 chnl=0", scale, trimRect.Width * scale / 2 + scale, (ushort)ch);
							}
							else
							{
								Rectangle rect = charRectList[index];
								if (nextX + rect.Width * scale > totalWidth)
								{
									nextX = 0;
									nextY += trimRect.Height * scale;
									if (nextY + trimRect.Height > totalHeight)
									{
										throw new ArgumentOutOfRangeException("font", "Too many characters or too large font size.");
									}
								}
								writer.WriteLine("char id={2} x={3} y={4} width={5} height={6} xoffset=0 yoffset={0} xadvance={1} page=0 chnl=0", scale, rect.Width * scale + scale, (ushort)ch, nextX, nextY, rect.Width * scale, trimRect.Height * scale);
								this.GenarateBitmap(bitmap, new Rectangle(rect.X, trimRect.Y, rect.Width, trimRect.Height), scale, needAddGap, nextX, nextY);
								nextX += rect.Width * scale;
							}

							index++;
						}
						bitmap.Save(font.Name + ".png", ImageFormat.Png);
						bitmap.Dispose();
					}
				}
			}
		}

		private void DrawChar(Graphics graphics, string str, Font font, int effect)
		{
			switch (effect)
			{
				case 0:
					graphics.DrawString(str, font, whiteBrush, 0, 0);
					break;
				case 1:
					graphics.DrawString(str, font, blackBrush, 0, 1);
					graphics.DrawString(str, font, blackBrush, 1, 0);
					graphics.DrawString(str, font, blackBrush, 1, 1);
					graphics.DrawString(str, font, whiteBrush, 0, 0);
					break;
				case 2:
					graphics.DrawString(str, font, blackBrush, 0, 0);
					graphics.DrawString(str, font, blackBrush, 0, 2);
					graphics.DrawString(str, font, blackBrush, 2, 0);
					graphics.DrawString(str, font, blackBrush, 2, 2);
					graphics.DrawString(str, font, blackBrush, 0, 1);
					graphics.DrawString(str, font, blackBrush, 1, 0);
					graphics.DrawString(str, font, blackBrush, 1, 2);
					graphics.DrawString(str, font, blackBrush, 2, 1);
					graphics.DrawString(str, font, whiteBrush, 1, 1);
					break;
			}
		}

		private Rectangle GetTrimRect(int width, int height)
		{
			int top = -1, bottom = -1, left = -1, right = -1;
			for (int y = height - 1; y >= 0; y--)
			{
				if (!this.IsRowEmpty(y, width))
				{
					bottom = y;
					break;
				}
			}
			for (int y = 0; y < height; y++)
			{
				if (!this.IsRowEmpty(y, width))
				{
					top = y;
					break;
				}
			}
			if (top < 0 || bottom < 0 || bottom < top)
			{
				return Rectangle.Empty;
			}
			for (int x = width - 1; x >= 0; x--)
			{
				if (!this.IsColumnEmpty(x, bottom + 1))
				{
					right = x;
					break;
				}
			}
			for (int x = 0; x < width; x++)
			{
				if (!this.IsColumnEmpty(x, bottom + 1))
				{
					left = x;
					break;
				}
			}
			if (left < 0 || right < 0 || right < left)
			{
				return Rectangle.Empty;
			}
			return new Rectangle(left, top, right - left + 1, bottom - top + 1);
		}

		private bool IsRowEmpty(int y, int width)
		{
			bool isEmpty = true;
			for (int x = 0; x < width; x++)
			{
				if (Color2Bit(bufferBitmap.GetPixel(x, y)) != 0)
				{
					isEmpty = false;
					break;
				}
			}
			return isEmpty;
		}

		private bool IsColumnEmpty(int x, int height)
		{
			bool isEmpty = true;
			for (int y = 0; y < height; y++)
			{
				if (Color2Bit(bufferBitmap.GetPixel(x, y)) != 0)
				{
					isEmpty = false;
					break;
				}
			}
			return isEmpty;
		}

		private void GenarateData(byte[] data, Rectangle rect, int wide, int scale)
		{
			data[0] = (byte)(rect.Width * scale);
			int length = data.Length;
			for (int i = 1; i < length; i++)
			{
				data[i] = 0;
			}
			for (int y = 0; y < rect.Height; y++)
			{
				for (int x = 0; x < rect.Width; x++)
				{
					if (scale == 1)
					{
						SetBit(data, x + y * wide * 4, Color2Bit(bufferBitmap.GetPixel(x + rect.X, y + rect.Y)));
					}
					else if (scale == 2)
					{
						int dX = x * 2;
						int dY = y * 2;
						int bit = Color2Bit(bufferBitmap.GetPixel(x + rect.X, y + rect.Y));
						SetBit(data, dX + dY * wide * 4, bit);
						SetBit(data, dX + 1 + dY * wide * 4, bit);
						SetBit(data, dX + (dY + 1) * wide * 4, bit);
						SetBit(data, dX + 1 + (dY + 1) * wide * 4, bit);
					}
				}
			}
		}

		private void GenarateBitmap(Bitmap bitmap, Rectangle rect, int scale, bool needAddGap, int startX, int startY)
		{
			int size = needAddGap ? scale - 1 : scale;
			for (int y = 0; y < rect.Height; y++)
			{
				for (int x = 0; x < rect.Width; x++)
				{
					int bit = Color2Bit(bufferBitmap.GetPixel(x + rect.X, y + rect.Y));
					if (bit == 1)
					{
						int dX = x * scale + startX;
						int dY = y * scale + startY;
						for (int subX = 0; subX < size; subX++)
						{
							for (int subY = 0; subY < size; subY++)
							{
								bitmap.SetPixel(dX + subX, dY + subY, Color.White);
							}
						}
					}
					else if (bit == 2)
					{
						int dX = x * scale + startX;
						int dY = y * scale + startY;
						for (int subX = 0; subX < size; subX++)
						{
							for (int subY = 0; subY < size; subY++)
							{
								bitmap.SetPixel(dX + subX, dY + subY, Color.Black);
							}
						}
					}
				}
			}
		}

		private static int Color2Bit(Color color)
		{
			if (color.B < 128)
			{
				if (color.G < 128)
				{
					return 2;
				}
				return 0;
			}
			else
			{
				if (color.G >= 128)
				{
					return 1;
				}
				return 0;
			}
		}

		private static int GetMemoryAlignment(int value)
		{
			return ((value + 3) / 4);
		}

		private static void SetBit(byte[] data, int index, int value)
		{
			int position = index / 4 + CharDataHeaderSize;
			int move = (index % 4) * 2;
			data[position] = (byte)(data[position] | (value << move));
		}
	}
}